package org.callisto.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Observable;
import java.util.Vector;

import javax.sound.midi.MidiMessage;
import javax.sound.midi.Receiver;

import org.callisto.Constants;
import org.callisto.EventType;
import org.callisto.Globals;
import org.callisto.MeasureType;
import org.callisto.StatisticType;

public class Keyboard extends Observable implements Receiver  {
	
	static long tickCount = 0;
	static long lastTickCount = 0;
	static long startTimeMillis = 0;
	static long lastEventMillis = 0;
	static boolean exitFlag = false;
	static int keystates[];
	static long keytimes[];
	static Vector<Event> events;
	static Vector<Event> noteOnEvents;
	static Vector<Event> noteOffEvents;
	static List<Event> delayList; static DurationComparator durationComparator = new DurationComparator();
	static List<Event> pressureList; static PressureComparator pressureComparator = new PressureComparator();
	static List<Event> noteList; static NoteComparator noteComparator = new NoteComparator();
	static HashMap<String,Float> statsMap;
	
	public int getEventCount() {
		return events.size();
	}
	
	public int getEventCount(EventType eventType) {
		int count=0;
		switch (eventType) {
		case NOTE_ON:
			count=noteOnEvents.size(); break;
		case NOTE_OFF:
			count=noteOffEvents.size();	break;
		}
		return count;
	}
	
	public Event getEvent(int index) {
		return events.get(index);
	}
	
	public Event getLastEvent(EventType eventType) {
		Event event = null;
		switch (eventType) {
		case NOTE_ON:
			event = (!noteOnEvents.isEmpty()) ? noteOnEvents.lastElement() : null;
			break;
		case NOTE_OFF:
			event = (!noteOffEvents.isEmpty()) ? noteOffEvents.lastElement() : null;
			break;
		}
		return event;
	}

	public Event getEvent(EventType eventType,int index) {
		Event event=null;
		switch (eventType) {
		case NOTE_ON:
			event=noteOnEvents.get(index); break;
		case NOTE_OFF:
			event=noteOffEvents.get(index); break;
		}
		return event;
	}
	
	public float getMeasureStatistic(EventType eventType, StatisticType statType) {
		float output=0;
		MeasureType measureType = null;
		switch (eventType) {
		case NOTE_ON:
			measureType = MeasureType.PRESSURE;
			break;
		case NOTE_OFF:
			measureType = MeasureType.DELAY;
			break;
		}
		output = statsMap.get(measureType + ":" + statType);
		Globals.logln("Getting " + measureType + ":" + statType + "[" + eventType + "] = " + output);
		return output;
	}
	
	public void updateStats(Event event) {
		Globals.logln("Updating stats");
		float medianIndex = 0;
		MeasureType measureType;
		Vector<Event> eventVector;
		List<Event> eventList;
		Comparator<Event> comparator;
		if (event.getStatus() == EventType.NOTE_ON) {
			measureType = MeasureType.PRESSURE;
			eventVector = noteOnEvents;
			eventList = pressureList;
			comparator = pressureComparator;
		} else {
			measureType = MeasureType.DELAY;
			eventVector = noteOffEvents;
			eventList = delayList;
			comparator = durationComparator;
		}
		int lastEventIndex=eventVector.size()-1;
		if (lastEventIndex >= 0) {
			int firstEventIndex=(lastEventIndex > Constants.STAT_SAMPLE_COUNT) ? lastEventIndex - Constants.STAT_SAMPLE_COUNT : 0;
			float acc=0;
			Collections.sort(eventList,comparator);
			Globals.log(measureType + " stats: {" + firstEventIndex + ".." + lastEventIndex + "} [ ");
			for (int i=firstEventIndex; i <= lastEventIndex; i++) {
				float measure=eventList.get(i).getMeasure();
				Globals.log(measure+ ",");
				acc += measure;
			}
			Globals.log("] [(mean,median,min,max): (");
					
			StatisticType statType = StatisticType.MEAN;
			Float measureValue = new Float(0f);
			measureValue = acc / ((lastEventIndex > Constants.STAT_SAMPLE_COUNT )  ? Constants.STAT_SAMPLE_COUNT : lastEventIndex + 1);
			statsMap.put(measureType + ":" + statType, measureValue);
			Globals.log(measureValue + ",");
			
			medianIndex = (float)(firstEventIndex + lastEventIndex ) / 2;
			statType = StatisticType.MEDIAN;
			measureValue = (Math.round(medianIndex) == Math.floor(medianIndex)) ? 
					eventList.get(Math.round(medianIndex)).getMeasure() : 
					((float) eventList.get((int) Math.floor(medianIndex)).getMeasure() + eventList.get((int) (Math.floor(medianIndex)+1)).getMeasure())/2;
			statsMap.put(measureType + ":" + statType, measureValue);
			Globals.log(measureValue + ",");
			
			statType = StatisticType.MIN;
			measureValue = (float) eventList.get(firstEventIndex).getMeasure();
			statsMap.put(measureType + ":" + statType, measureValue);
			Globals.log(measureValue + ",");
			
			statType = StatisticType.MAX;
			measureValue = (float) eventList.get(lastEventIndex).getMeasure();
			statsMap.put(measureType + ":" + statType, measureValue);
			Globals.logln(measureValue + ")]");
		}
	}
	
	public void send(MidiMessage m, long t){
		try {
			EventType status = EventType.get(m.getStatus());	
			switch (status) {
			case TIMING_CLOCK:
				tickCount++;
				break;
			case NOTE_ON:
				break;
			case NOTE_OFF:		
				break;
			}
			
			if (status == EventType.NOTE_ON || status == EventType.NOTE_OFF) {
				// capture timestamps
				long currentTimeMillis = System.currentTimeMillis();
				java.util.Date today = new java.util.Date();
				java.sql.Timestamp tstamp = new java.sql.Timestamp(today.getTime());
				if ( (currentTimeMillis - lastEventMillis)/1000 > Constants.SESSION_TIMEOUT_SECS ) {
					if (lastEventMillis > 0) {
						System.out.println("Session timeout (" + Constants.SESSION_TIMEOUT_SECS + "s) exceeded. Time since last event: " + (currentTimeMillis - lastEventMillis)/1000 + "s");
					}
					System.out.println("Resetting counters");
					startTimeMillis = currentTimeMillis;
					tickCount = 0;
					lastTickCount = 0;
					lastEventMillis = currentTimeMillis;
					keystates = new int[Constants.KEYBOARD_WIDTH];
					keytimes = new long[Constants.KEYBOARD_WIDTH];
					events=new Vector<Event>();
					noteOnEvents = new Vector<Event>();
					noteOffEvents = new Vector<Event>();
					delayList = new ArrayList<Event>();
					pressureList = new ArrayList<Event>();
					noteList = new ArrayList<Event>();
					statsMap = new HashMap<String,Float>();
				} 
				lastEventMillis = currentTimeMillis;
				long millis = currentTimeMillis - startTimeMillis;
				byte note = m.getMessage()[1];
				int pressure = m.getMessage()[2];
				long last_millis = keytimes[note];
				keytimes[note] = millis;
				int delay;
				if (last_millis == 0 && status == EventType.NOTE_ON) {
					delay=-1; // set delay to -1 if first event in session for key; otherwise to delta
				} else {
					delay=(int) (millis - last_millis);
				}
				Event event = new Event(millis, tstamp, note, status, pressure, delay);
				events.add(event);
				if (status == EventType.NOTE_ON) {
					keystates[note] = pressure;
					noteOnEvents.add(event);
					pressureList.add(event);
					noteList.add(event);
				}
				if (status == EventType.NOTE_OFF) {
					keystates[note] = 0;
					noteOffEvents.add(event);
					delayList.add(event);
				}
				updateStats(event);
				setChanged();
				notifyObservers(event);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void close(){
		System.out.println("Closing MIDI interface");
	}
}